Skip to content

feat(cli): stash wizard thin-wrapper subcommand#394

Open
coderdan wants to merge 2 commits intomainfrom
dan/stash-wizard-wrapper
Open

feat(cli): stash wizard thin-wrapper subcommand#394
coderdan wants to merge 2 commits intomainfrom
dan/stash-wizard-wrapper

Conversation

@coderdan
Copy link
Copy Markdown
Contributor

@coderdan coderdan commented May 2, 2026

Summary

Re-adds stash wizard as a thin spawn wrapper around @cipherstash/wizard. The wizard ships as a separate npm package (the agent SDK is too heavy to bundle into stash); this wrapper exposes it as a stash subcommand so users only have to think about one CLI surface.

  • Detects the project's package manager and spawns via the matching one-shot runner: npx, pnpm dlx, yarn dlx, or bunx — with stdio: 'inherit'.
  • Prints "first run downloads ~5s" on a cold cache so the CLI doesn't appear hung while the package manager resolves the package; warm-cache runs print a short "Launching..." line before the wizard takes over.
  • Forwards anything after wizard verbatim (so stash wizard --debug works).
  • Updates copy that pointed at npx @cipherstash/wizard (init's next-steps for base / Drizzle / Supabase, db install's post-install note, the help banner) to stash wizard.

This is the prerequisite PR for #395 — the new init flow's "Use the CipherStash Agent" handoff option calls into the same wrapper.

Why a wrapper instead of folding the wizard back in

CJ split the wizard out in #368 (May 1) precisely to keep the stash CLI bundle lean. Pulling the agent SDK back in would undo that. The wrapper is one spawn call — the wizard package is still optional and only resolved at run-time.

Test plan

  • pnpm --filter stash test (123 tests, including 6 new for runner-string splitting)
  • pnpm --filter stash build clean
  • E2E suite: pnpm --filter @cipherstash/e2e test:e2e (21 tests including the runner-string assertion that initially caught a missed copy update — now fixed)
  • Manual: node packages/cli/dist/bin/stash.js wizard in a fresh project — cold-cache message + spawn confirmed
  • Manual: stash --help shows the wizard line + example

Files changed

  • packages/cli/src/commands/wizard/index.ts — new
  • packages/cli/src/bin/stash.ts — case + help banner
  • packages/cli/src/commands/index.ts — export
  • packages/cli/src/commands/init/providers/{base,drizzle,supabase}.ts — replace npx @cipherstash/wizard reference with stash wizard
  • packages/cli/src/commands/db/install.ts — same replacement in the post-install note
  • packages/cli/src/commands/init/providers/__tests__/*.test.ts — assertion updates
  • e2e/tests/package-managers.e2e.test.ts — assertion update
  • .changeset/cli-stash-wizard-subcommand.md — minor bump

@coderdan coderdan requested a review from a team as a code owner May 2, 2026 06:04
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 2, 2026

🦋 Changeset detected

Latest commit: e999857

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
stash Minor
@cipherstash/e2e Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

The PR adds a new wizard CLI subcommand that wraps the standalone @cipherstash/wizard package. The wrapper detects the project package manager and spawns the wizard via the corresponding one-shot runner (npx, pnpm dlx, yarn dlx, or bunx) with inherited stdio, forwarding arguments. All references to @cipherstash/wizard in init provider messaging are updated to point to stash wizard.

Changes

Wizard Subcommand Implementation & Integration

Layer / File(s) Summary
Core Implementation
packages/cli/src/commands/wizard/index.ts
Introduces wizardCommand() that detects package manager, builds runner command, checks cache status, spawns wizard with stdio: 'inherit', and propagates exit codes. Exports splitRunner() to parse runner commands into executable and pre-args tokens.
Command Wiring
packages/cli/src/commands/index.ts, packages/cli/src/bin/stash.ts
Re-exports wizardCommand from commands module. Adds wizard case to CLI dispatcher, forwards process.argv.slice(3) to command, updates help text and examples.
Message Updates
packages/cli/src/commands/init/providers/base.ts, packages/cli/src/commands/init/providers/drizzle.ts, packages/cli/src/commands/init/providers/supabase.ts, packages/cli/src/commands/db/install.ts
Updates "next steps" messaging to reference stash wizard (via ${cli} wizard) instead of separate @cipherstash/wizard runner invocations.
Tests & Documentation
packages/cli/src/commands/wizard/__tests__/index.test.ts, packages/cli/src/commands/init/providers/__tests__/{base,drizzle,supabase}.test.ts, .changeset/cli-stash-wizard-subcommand.md
New test suite for splitRunner covering single/multi-token runners, whitespace normalization, and error cases. Existing provider tests updated to expect @cipherstash/cli wizard in next-steps assertions. Changesets entry documents the feature and backward-compatibility messaging.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI User
    participant Stash as stash wizard<br/>Command
    participant PM as Package Manager<br/>Detector
    participant FS as File System<br/>(cache check)
    participant Spawn as Node spawn()
    participant Wizard as `@cipherstash/wizard`<br/>Process

    CLI->>Stash: stash wizard [args...]
    Stash->>PM: Detect project PM<br/>(npm/pnpm/yarn/bun)
    PM-->>Stash: npm | pnpm | yarn | bun
    Stash->>Stash: Build runner cmd<br/>(npx | pnpm dlx | yarn dlx | bunx)
    Stash->>FS: Check if wizard cached
    alt First Run (not cached)
        FS-->>Stash: Not found
        Stash->>CLI: Print "downloading (~5s)..."
    else Warm Cache
        FS-->>Stash: Found
        Stash->>CLI: Print "Launching wizard..."
    end
    Stash->>Spawn: spawn(bin, [preArgs, '@cipherstash/wizard', ...args],<br/>{ stdio: 'inherit' })
    Spawn->>Wizard: Start with inherited TTY
    Wizard->>Wizard: User interaction
    Wizard->>Spawn: Exit with code
    Spawn-->>Stash: close event + exit code
    Stash->>CLI: process.exit(code)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • auxesis
  • calvinbrewer

Poem

🐰 A wizard subcommand hops into place,
Detects your PM with elegant grace,
Spawns with inherited TTY so bright,
Forwards your args without a fight!
No more npx @cipherstash/wizard`` to say—
Just stash wizard to magic away!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(cli): stash wizard thin-wrapper subcommand' accurately summarizes the main change: adding a new stash wizard subcommand as a thin wrapper around @cipherstash/wizard.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dan/stash-wizard-wrapper

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/cli/src/commands/db/install.ts (1)

271-288: ⚡ Quick win

printNextSteps hardcodes stash wizard instead of using the pm-aware runner.

All three init providers compute the wizard step as ${runnerCommand(pm, '@cipherstash/cli')} wizard, producing e.g. pnpm dlx @cipherstash/cli wizard for pnpm users. printNextSteps ignores the package manager entirely and emits a bare stash wizard, which is wrong for any user who ran db install via a one-shot runner (no global stash binary on PATH).

Both detectPackageManager and runnerCommand are already imported at line 5, so the fix is minimal:

♻️ Proposed fix
-function printNextSteps(): void {
+function printNextSteps(pm = detectPackageManager()): void {
+  const cli = runnerCommand(pm, '@cipherstash/cli')
   p.note(
     [
       'Next steps:',
       '',
       '  1. Wire up encrypt/decrypt with the wizard (AI-guided, automated):',
-      '       stash wizard',
+      `       ${cli} wizard`,
       '',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/db/install.ts` around lines 271 - 288,
printNextSteps currently hardcodes the string "stash wizard" instead of using
the package-manager-aware runner; update printNextSteps to call
detectPackageManager() and compute the one-shot runner via runnerCommand(pm,
'@cipherstash/cli'), then use `${runner} wizard` (or equivalent) in the message
instead of the hardcoded "stash wizard" so the displayed next-step works for
pnpm/npm/yarn one-shot invocations; refer to the existing functions
detectPackageManager and runnerCommand and the printNextSteps function to locate
where to change the message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/bin/stash.ts`:
- Around line 274-281: Add an E2E test that exercises the new `wizard` routing
in `stash.ts` by spawning the CLI and asserting exit codes: call the binary with
arguments ["wizard","--help"] and assert it exits with code 0, and call a
failing/unknown wizard invocation (e.g., ["wizard","--invalid-flag"] or a wizard
mode that you know returns non-zero) and assert the process exit code is the
expected non-zero value; use Node's child_process.spawn/execFile in a Jest/Mocha
test to capture exit events and stdout/stderr, and reference the `wizardCommand`
behavior so the test ensures exit-code propagation through the `case 'wizard'`
branch.

In `@packages/cli/src/commands/wizard/index.ts`:
- Around line 60-67: The Windows failure is caused by using node's
child_process.spawn (spawn) with shell: false which cannot execute package
manager .cmd shims (npm/pnpm/yarn); replace the use of spawn in this block with
cross-spawn's default export (import spawn from 'cross-spawn') and call it with
the same signature (spawn(bin, args, { stdio: 'inherit' })) so .cmd is appended
on Windows; keep the child.on('close', ...) and child.on('error', ...) handlers
and the exitCode Promise logic unchanged and ensure TypeScript types/imports are
updated to reference cross-spawn instead of child_process.spawn.

---

Nitpick comments:
In `@packages/cli/src/commands/db/install.ts`:
- Around line 271-288: printNextSteps currently hardcodes the string "stash
wizard" instead of using the package-manager-aware runner; update printNextSteps
to call detectPackageManager() and compute the one-shot runner via
runnerCommand(pm, '@cipherstash/cli'), then use `${runner} wizard` (or
equivalent) in the message instead of the hardcoded "stash wizard" so the
displayed next-step works for pnpm/npm/yarn one-shot invocations; refer to the
existing functions detectPackageManager and runnerCommand and the printNextSteps
function to locate where to change the message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2fb4021c-5b21-4d68-9ff1-8efa18f0fc80

📥 Commits

Reviewing files that changed from the base of the PR and between af78001 and 4ab8e16.

📒 Files selected for processing (12)
  • .changeset/cli-stash-wizard-subcommand.md
  • packages/cli/src/bin/stash.ts
  • packages/cli/src/commands/db/install.ts
  • packages/cli/src/commands/index.ts
  • packages/cli/src/commands/init/providers/__tests__/base.test.ts
  • packages/cli/src/commands/init/providers/__tests__/drizzle.test.ts
  • packages/cli/src/commands/init/providers/__tests__/supabase.test.ts
  • packages/cli/src/commands/init/providers/base.ts
  • packages/cli/src/commands/init/providers/drizzle.ts
  • packages/cli/src/commands/init/providers/supabase.ts
  • packages/cli/src/commands/wizard/__tests__/index.test.ts
  • packages/cli/src/commands/wizard/index.ts

Comment on lines +274 to +281
case 'wizard': {
// Forward everything after `stash wizard` verbatim. The wizard package
// owns its own flag parsing; we don't try to interpret its surface
// here so it can evolve independently.
const wizardArgs = process.argv.slice(3)
await wizardCommand(wizardArgs)
break
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

E2E test required by coding guidelines for this routing change.

The wizard case introduces a new exit-code propagation path (wizardCommand calls process.exit(exitCode)) routed through stash.ts. The coding guidelines explicitly require an E2E test when touching src/bin/stash.ts "argv parsing, exit codes, or top-level error handling". The PR's test plan covers only unit tests for splitRunner; no E2E test for stash wizard routing is listed.

At minimum, an E2E test should verify that stash wizard --help (or equivalent) exits 0 and that an unknown/failing wizard invocation propagates the expected non-zero exit code.

As per coding guidelines: "Add an E2E test when touching src/bin/stash.ts argv parsing, exit codes, or top-level error handling."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/bin/stash.ts` around lines 274 - 281, Add an E2E test that
exercises the new `wizard` routing in `stash.ts` by spawning the CLI and
asserting exit codes: call the binary with arguments ["wizard","--help"] and
assert it exits with code 0, and call a failing/unknown wizard invocation (e.g.,
["wizard","--invalid-flag"] or a wizard mode that you know returns non-zero) and
assert the process exit code is the expected non-zero value; use Node's
child_process.spawn/execFile in a Jest/Mocha test to capture exit events and
stdout/stderr, and reference the `wizardCommand` behavior so the test ensures
exit-code propagation through the `case 'wizard'` branch.

Comment on lines +60 to +67
const exitCode = await new Promise<number>((resolvePromise) => {
const child = spawn(bin, args, { stdio: 'inherit', shell: false })
child.on('close', (code) => resolvePromise(code ?? 0))
child.on('error', (err) => {
p.log.error(`Failed to launch wizard: ${err.message}`)
resolvePromise(127)
})
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check package.json engines field and any CI matrix for Windows runners
rg -n '"engines"' packages/cli/package.json
fd -e yml -e yaml . .github/workflows --exec grep -l 'windows'

Repository: cipherstash/stack

Length of output: 78


🏁 Script executed:

cat packages/cli/package.json | head -100

Repository: cipherstash/stack

Length of output: 1741


🏁 Script executed:

rg -A 5 '"engines"' packages/cli/package.json

Repository: cipherstash/stack

Length of output: 97


🏁 Script executed:

cat -n packages/cli/src/commands/wizard/index.ts | sed -n '50,75p'

Repository: cipherstash/stack

Length of output: 925


🏁 Script executed:

rg -B 5 -A 15 'function splitRunner|export.*splitRunner|const splitRunner' packages/cli/src/commands/wizard/

Repository: cipherstash/stack

Length of output: 2181


🏁 Script executed:

rg -n "splitRunner" packages/cli/src/commands/wizard/index.ts

Repository: cipherstash/stack

Length of output: 212


🏁 Script executed:

rg -B 10 "const runner = " packages/cli/src/commands/wizard/index.ts

Repository: cipherstash/stack

Length of output: 622


🏁 Script executed:

rg -B 20 "function splitRunner" packages/cli/src/commands/wizard/index.ts | head -40

Repository: cipherstash/stack

Length of output: 790


🏁 Script executed:

rg -A 30 "function runnerCommand|export.*runnerCommand" packages/cli/src/commands/init/utils.ts

Repository: cipherstash/stack

Length of output: 745


🏁 Script executed:

rg -A 20 "function detectPackageManager|export.*detectPackageManager" packages/cli/src/commands/init/utils.ts

Repository: cipherstash/stack

Length of output: 734


🏁 Script executed:

rg -n "windows|Windows|WIN32|win32" packages/cli/README.md packages/cli/CHANGELOG.md 2>/dev/null

Repository: cipherstash/stack

Length of output: 43


🏁 Script executed:

find packages/cli -name "*.md" -type f | xargs grep -l "Windows\|windows\|platform" 2>/dev/null

Repository: cipherstash/stack

Length of output: 45


🏁 Script executed:

rg "spawn.*bin.*shell.*false" packages/cli/src/ -A 5 -B 5

Repository: cipherstash/stack

Length of output: 944


spawn with shell: false will fail on Windows for npm, pnpm, and yarn.

These package managers are distributed as .cmd batch scripts on Windows, and spawn with shell: false cannot execute them directly — the same ENOENT behavior is confirmed in the Node.js ecosystem. Only bunx (a native .exe) works as-is.

While Windows support is not explicitly declared in the engines field, the code will fail on Windows regardless. The standard fix in the Node.js ecosystem is to replace spawn with cross-spawn, which transparently appends .cmd on Windows:

🐛 Proposed fix using cross-spawn
-import { spawn } from 'node:child_process'
+import spawn from 'cross-spawn'

cross-spawn is a drop-in replacement with identical call signature and is already a common dependency in CLI tooling. Alternatively, if adding a new dependency is undesirable, a minimal inline shim works:

-    const child = spawn(bin, args, { stdio: 'inherit', shell: false })
+    const winBin = process.platform === 'win32' ? `${bin}.cmd` : bin
+    const child = spawn(winBin, args, { stdio: 'inherit', shell: false })

Note: shell: true is not a safe workaround because passthroughArgs are user-supplied and would be subject to shell injection.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const exitCode = await new Promise<number>((resolvePromise) => {
const child = spawn(bin, args, { stdio: 'inherit', shell: false })
child.on('close', (code) => resolvePromise(code ?? 0))
child.on('error', (err) => {
p.log.error(`Failed to launch wizard: ${err.message}`)
resolvePromise(127)
})
})
const exitCode = await new Promise<number>((resolvePromise) => {
const winBin = process.platform === 'win32' ? `${bin}.cmd` : bin
const child = spawn(winBin, args, { stdio: 'inherit', shell: false })
child.on('close', (code) => resolvePromise(code ?? 0))
child.on('error', (err) => {
p.log.error(`Failed to launch wizard: ${err.message}`)
resolvePromise(127)
})
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/wizard/index.ts` around lines 60 - 67, The Windows
failure is caused by using node's child_process.spawn (spawn) with shell: false
which cannot execute package manager .cmd shims (npm/pnpm/yarn); replace the use
of spawn in this block with cross-spawn's default export (import spawn from
'cross-spawn') and call it with the same signature (spawn(bin, args, { stdio:
'inherit' })) so .cmd is appended on Windows; keep the child.on('close', ...)
and child.on('error', ...) handlers and the exitCode Promise logic unchanged and
ensure TypeScript types/imports are updated to reference cross-spawn instead of
child_process.spawn.

The wizard ships as @cipherstash/wizard so the agent SDK stays out of the
CLI bundle. Re-add a `stash wizard` subcommand that spawns the wizard via
the project's package manager (npx / pnpm dlx / yarn dlx / bunx) so users
only have to think about one CLI surface.

Flags after `wizard` are forwarded verbatim. Cold-cache runs print an
explicit "first run downloads ~5s" line so the CLI doesn't appear hung
while the package manager resolves the package; warm-cache runs print a
single "Launching..." line and hand the terminal over to the wizard.

Existing copy that pointed at `npx @cipherstash/wizard` (init's next-steps
for base/Drizzle/Supabase, db install's post-install note, the help banner)
now uses `stash wizard`.
@coderdan coderdan force-pushed the dan/stash-wizard-wrapper branch from 4ab8e16 to ce70b4d Compare May 2, 2026 06:16
The wrapper changes the runner-aware string the providers emit from
`<runner> @cipherstash/wizard` to `<runner> stash wizard`. The provider
unit tests were updated; the e2e suite checked the wizard hint via a
`steps.find` and was missed. Updates the expectation so the find lands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant